Skip to content

feat(chart): chart-view redesign — config bar, autoChart, 5 types via Chart.js#20

Merged
BorisTyshkevich merged 8 commits into
mainfrom
feat/chart-views
Jun 23, 2026
Merged

feat(chart): chart-view redesign — config bar, autoChart, 5 types via Chart.js#20
BorisTyshkevich merged 8 commits into
mainfrom
feat/chart-views

Conversation

@BorisTyshkevich

Copy link
Copy Markdown
Collaborator

What

Replaces the single hard-wired bar chart with a real chart surface, matching the Claude Design handoff.

  • Config bar: Type (Bar = horizontal / Column / Line / Area / Pie), X, Y, an "All measures" multi-series toggle, and a group-by Series dropdown.
  • autoChart heuristic — classifies each column from its ClickHouse type (stripping Nullable/LowCardinality): temporal X → line, categorical X → horizontal bar, ordinal X → column. Non-chartable results show a hint instead of a broken axis.
  • Five renderers + multi-series and group-by pivot, themed to the CSS tokens (accent series, mono ticks, humanized K/M + YYYY-MM labels).
  • Chart config persists per query tab (re-derived on schema change, overrides kept otherwise).

Dependency decision

Rendering now uses Chart.js — the one bundled runtime dependency, inlined into dist/sql.html, so the page still makes zero third-party requests. This relaxes CLAUDE.md hard rule #4 (no runtime deps); the change is recorded in CLAUDE.md, README.md, and build/build.mjs. React-only libs (Recharts/visx) were ruled out — the app is framework-free.

Keeping the layers honest

All role/axis/pivot/scale math and the Chart.js config builder are pure in src/core/chart-data.js (100/100/100/100); the new Chart() call is an injected seam (app.Chart, like the fetch/crypto seams), so results.js stays fully tested. Net: coverage never had to drop — only the no-deps rule did. Live Chart instances are destroyed on re-render to avoid canvas/observer leaks.

Verification

  • npm test488 passed, per-file coverage gate green.
  • npm run build313 KB self-contained dist/sql.html (Chart.js inlined).
  • Rendered all 7 chart shapes in a browser against ClickHouse-shaped data — autoChart picks the right default per data shape; every type draws correctly.

🤖 Generated with Claude Code

…s via Chart.js

Replace the single hard-wired bar chart with a real chart surface matching the
Claude Design handoff: a config bar (Type / X / Y / multi-series toggle /
group-by Series), an autoChart heuristic that classifies columns from their
ClickHouse types (temporal → line, categorical → horizontal bar, ordinal →
column), and five renderers (h-bar / column / line / area / pie) with
multi-series and group-by pivot, themed to the CSS tokens.

Rendering now uses Chart.js — the one bundled runtime dependency, inlined into
dist/sql.html so the page still makes zero third-party requests. This relaxes
CLAUDE.md hard rule #4 (recorded there, in the README, and build.mjs). To keep
the coverage model intact, all role/axis/pivot/scale math and the Chart.js
config builder are pure in src/core/chart-data.js (100%), and the `new Chart()`
call is an injected seam (app.Chart) so the DOM wrapper stays fully tested.

Chart config persists per query tab (tab.chartCfg/chartKey), re-derived on
schema change; live Chart instances are destroyed on re-render to avoid leaks.

npm test: 488 passed, per-file gate green. Build: 313 KB self-contained artifact.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wtzg34E1iGzKm11TArRpko
BorisTyshkevich and others added 7 commits June 22, 2026 20:59
…t, tighten "All measures"

- Show "first 500 of N rows" in the config bar when a result exceeds the chart's
  CHART_ROW_CAP, so the truncation is never silent (the table still shows all).
- chartRole reuses format.js:isNumericType on the stripped type instead of a
  second copy of the numeric regex.
- "All measures" now targets only true measures and excludes the current X
  column, so it can't plot an ordinal axis (e.g. month) against itself.
- Comment chartNumFmt's deliberate divergence from formatRows.

(Reviewer's sort-on-every-config-click note left as-is: sortRows no-ops without
an active sort, and caching only the chart's sort would diverge from the table.)

npm test: 490 passed, per-file gate green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wtzg34E1iGzKm11TArRpko
`.res-body` is display:block, so `.chart-view { flex: 1 }` was inert: the view
collapsed to the config bar's height and Chart.js sized the canvas to a few
pixels — a config bar with no visible chart. Use `height: 100%` (the block
parent has a definite height) for `.chart-view` and `.chart-empty` so the
canvas wrapper gets real height.

Not caught earlier: happy-dom does no layout, and the manual browser harness
attached the canvas into a pre-sized container, bypassing the real `.res-body`
parent. Verified live against an otel system.query_log result.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wtzg34E1iGzKm11TArRpko
Vitest 2.x defaults to `pool: 'forks'`, which fans out to (cpus-1) child
node processes via tinypool — 11 on a 12-core box. On normal exit those
should be reaped, but a detached swarm can survive a run and accumulate
across runs until it saturates CPU/RAM.

Switch to `pool: 'threads'` (capped at 4): worker threads live inside the
single vitest process, so they die with the parent and can never become
orphaned OS processes. Safe here — the suite is pure ES modules + happy-dom
with no native deps. Also faster (happy-dom env setup 21.7s -> 3.0s, total
6.1s -> 2.3s) since threads skip per-fork environment spin-up.

Belt-and-suspenders: a `pretest` hook kills any pre-existing stray vitest
processes before a run.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016SGQKuUW7a12hBSwmwSkYa
…bugs

Persistence
- Save/restore the per-tab chart config (type/X/Y/multi/series) with saved
  queries and share links. Saved entries gain an optional { cfg, key }; share
  links carry a tagged JSON envelope (back-compatible with legacy SQL-only
  hashes); export/import round-trip it. Configs are deep-cloned on save/restore
  so a restored chart can't mutate its source, and a chartCfgValid guard rejects
  out-of-range indices from a hand-edited link/import.

Chart-view fixes
- chartLabel keeps day granularity (YYYY-MM-DD, display-only); buildChartData
  SUM-aggregates per cell instead of last-wins, so duplicate buckets combine.
- ORDINAL_RE anchored (^bucketword s?$) so measures like monthly_revenue are no
  longer misread as ordinal axes.
- running guard runs before chartCfgFor, so a still-settling result can't stamp
  over a restored config.
- normalizeChartCfg clears a Series equal to X and folds pie to single-measure /
  no-series; chartCfgFor normalizes on every restore.
- renderChart plots rows in query order, independent of the table sort (also
  drops the wasteful full-result sort before the 500-row cap).
- chartColors resolves --mono to a real font stack for canvas ticks/legend.
- .chart-view scrolls and the canvas keeps a min height instead of crushing to 0.
- pretest pkill scoped to this project's vitest config.

523 tests pass; pure/render layers at 100/100/100/100. Verified e2e on antalya
against ontime.fact_ontime.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016SGQKuUW7a12hBSwmwSkYa
…ay labels, scope pretest

- chartCfgFor is now the single place that folds chart-config invariants; the
  Type/X/Series handlers just mutate + re-render (drop their redundant
  normalizeChartCfg calls).
- chartLabel keeps date+HH:MM for an intraday timestamp (date-only for a Date or
  midnight DateTime), so two same-day buckets no longer collapse to the same
  tick text. Grouping is still on the raw value. Removed the now-unused
  ISO_DATE_RE.
- pretest pkill pattern requires both `vitest` and `--config tests/vitest.config.ts`
  so it can't match an unrelated process that merely has the path in its argv.

525 tests pass; chart-data.js at 100/100/100/100.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016SGQKuUW7a12hBSwmwSkYa
- Saved queries now persist the result view (Table/JSON/Chart) alongside the
  chart config; saveQuery captures state.resultView (the transient raw view is
  not saved), and saved-io export/import/merge carry it.
- Opening a saved query reopens that view and runs immediately: the saved-row
  click calls run({ view }), and run() now starts in the given view (or Table
  when absent/unknown) instead of always resetting to Table. History rows also
  re-run on click (Table view).
- View ownership lives in run() (which already resets resultView); loadIntoNewTab
  stays responsible only for tab-level state (sql/savedId/chart).

528 tests pass; core/state/render layers at their per-file gates. Verified e2e
on antalya: a one-click saved-Chart query auto-runs straight into its chart.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016SGQKuUW7a12hBSwmwSkYa
…143)

`pkill -f` matches full command lines, so the hook's own shell — whose argv
contains the pattern string — was a match, and pkill SIGTERM'd its parent
(`npm test`) before `|| true` could run. CI fails at the pretest step with exit
143 before vitest starts; it only passed locally because I'd been running the
vitest binary directly, bypassing the npm lifecycle.

The hook guarded against the forks-pool orphan swarm, which `pool: 'threads'`
already eliminated (threads die with the parent — verified zero lingering
processes), so it's removed rather than patched.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016SGQKuUW7a12hBSwmwSkYa
@BorisTyshkevich BorisTyshkevich merged commit 6b4ecf2 into main Jun 23, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant